package com.hlg.xboot.service;

import com.hlg.xboot.core.annotation.IgnoreQuery;
import com.hlg.xboot.enums.QueryType;
import com.hlg.xboot.model.AbstractModel;
import com.hlg.xboot.utils.Utils;
import com.hlg.xboot.vo.QueryKeyWordVO;
import com.xiaoleilu.hutool.date.DateUtil;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.jpa.domain.Specification;

import javax.persistence.criteria.*;
import java.lang.reflect.Field;
import java.util.*;

/**
 * Created by yangxin on 16/4/4.
 */
public abstract class SpecificationService<E> {

    protected static final String KEYWORD_FIELD_NAME = "keyword";

    protected static final String RANGES_FIELD_NAME = "ranges";

    protected static final String QUERY_TYPE_NAME = "queryType";

    private Map<String, Integer> getTargetQueryType(E e) {
        HashMap<String, Integer> queryType = null;
        Class superClass = e.getClass();
        while (queryType == null && superClass != null && superClass != AbstractModel.class) {
            try {
                superClass = superClass.getSuperclass();
                Field field = superClass.getDeclaredField(QUERY_TYPE_NAME);
                field.setAccessible(true);
                queryType = (HashMap<String, Integer>) field.get(e);
            } catch (NoSuchFieldException e1) {

            } catch (IllegalAccessException e1) {
                e1.printStackTrace();
            } finally {

            }
        }
        return queryType;
    }

    protected Specification<E> buildSpecification(E e) {
        return new Specification<E>() {

            @Override
            public Predicate toPredicate(Root<E> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
                List<Predicate> andPredicates = new ArrayList<Predicate>();
                List<Predicate> orPredicates = new ArrayList<Predicate>();

                Map<String, Integer> queryType = getTargetQueryType(e);
                buildPredicates(root, cb, e, andPredicates, orPredicates, null, null, null, queryType);
                List<Predicate> predicates = new ArrayList<>();
                predicates.add(cb.and(andPredicates.toArray(new Predicate[andPredicates.size()])));
                if (orPredicates.size() > 0) {
                    predicates.add(cb.or(orPredicates.toArray(new Predicate[orPredicates.size()])));
                }
                return query.where(predicates.toArray(new Predicate[predicates.size()])).getRestriction();
            }

            private void buildPredicates(Root<E> root, CriteriaBuilder cb, Object target, List<Predicate> andPredicates,
                                         List<Predicate> orPredicates, String levelOneParentName, String levelTwoParent,
                                         String levelThreeParent, Map<String, Integer> queryType) {
                Field[] _fields = target.getClass().getDeclaredFields();
                Class superClass = target.getClass().getSuperclass();
                while (superClass != null && superClass != AbstractModel.class) {
                    Field[] superFileds = superClass.getDeclaredFields();
                    _fields = ArrayUtils.addAll(_fields, superFileds);
                    superClass = superClass.getSuperclass();
                }

                final Field[] fields = _fields;

                for (Field field : fields) {
                    if (field.getAnnotation(IgnoreQuery.class) != null) {
                        continue;
                    }
                    String name = field.getName();
                    try {
                        field.setAccessible(true);
                        Object value = field.get(target);
                        if (value == null || value instanceof Collection) {
                            continue;
                        }
                        if (value instanceof String && StringUtils.isEmpty(value.toString())) {
                            continue;
                        }
                        if (QUERY_TYPE_NAME.equals(name)) {
                            continue;
                        }
                        if (KEYWORD_FIELD_NAME.equals(name)) {
                            QueryKeyWordVO keyWord = (QueryKeyWordVO) value;
                            if (StringUtils.isEmpty(keyWord.getValue()) || ArrayUtils.isEmpty(keyWord.getFields())) {
                                continue;
                            }
                            for (String attribute : keyWord.getFields()) {
                                String[] attrs = attribute.split("\\.");
                                Path<String> ap = root.get(attrs[0]);
                                int index = 0;
                                while (++index < attrs.length) {
                                    ap = ap.get(attrs[index]);
                                }
                                orPredicates.add(cb.like(ap, Utils.likeQuery(keyWord.getValue())));
                            }
                            continue;
                        }
                        if (RANGES_FIELD_NAME.equals(name)) {
                            Map<String, String[]> ranges = (Map<String, String[]>) value;
                            if (ranges != null) {
                                ranges.entrySet().forEach((item) -> {
                                    String key = item.getKey();

                                    String[] attrs = key.split("\\.");

                                    Field keyField = getField(fields, attrs[0]);

                                    Path ap = root.get(attrs[0]);
                                    int index = 0;
                                    while (++index < attrs.length) {
                                        ap = ap.get(attrs[index]);
                                        try {
                                            keyField.setAccessible(true);
                                            keyField = keyField.getType().getDeclaredField(attrs[index]);
                                        } catch (NoSuchFieldException e1) {
                                            e1.printStackTrace();
                                        }
                                    }

                                    if (keyField != null) {
                                        String[] values = item.getValue();
                                        try {
                                            if (keyField.getType() == Integer.class) {
                                                if (StringUtils.isNotEmpty(values[0])) {
                                                    andPredicates.add(cb.greaterThanOrEqualTo(ap, Integer.parseInt(values[0])));
                                                }
                                                if (values.length > 1 && StringUtils.isNotEmpty(values[1])) {
                                                    andPredicates.add(cb.lessThanOrEqualTo(ap, Integer.parseInt(values[1])));
                                                }
                                            } else if (keyField.getType() == Double.class) {
                                                if (StringUtils.isNotEmpty(values[0])) {
                                                    andPredicates.add(cb.greaterThanOrEqualTo(ap, Double.parseDouble(values[0])));
                                                }
                                                if (values.length > 1 && StringUtils.isNotEmpty(values[1])) {
                                                    andPredicates.add(cb.lessThanOrEqualTo(ap, Double.parseDouble(values[1])));
                                                }
                                            } else if (keyField.getType() == Date.class) {
                                                if (StringUtils.isNotEmpty(values[0])) {
                                                    andPredicates.add(cb.greaterThanOrEqualTo(ap, DateUtil.parse(values[0]).toSqlDate()));
                                                }
                                                if (values.length > 1 && StringUtils.isNotEmpty(values[1])) {
                                                    andPredicates.add(cb.lessThanOrEqualTo(ap, DateUtil.parse(values[1]).toSqlDate()));
                                                }
                                            }
                                        } catch (Exception e) {
                                        }
                                    }
                                });
                            }
                            continue;
                        }
                        Path path;
                        if (StringUtils.isNotEmpty(levelOneParentName) && StringUtils.isNotEmpty(levelTwoParent) && StringUtils.isNotEmpty(levelThreeParent)) {
                            path = root.get(levelThreeParent).get(levelTwoParent).get(levelOneParentName).get(name);
                        } else if (StringUtils.isNotEmpty(levelTwoParent)) {
                            path = root.get(levelTwoParent).get(levelOneParentName).get(name);
                        } else if (StringUtils.isNotEmpty(levelOneParentName)) {
                            path = root.get(levelOneParentName).get(name);
                        } else {
                            path = root.get(name);
                        }

                        if (value instanceof AbstractModel) {
                            buildPredicates(root, cb, value, andPredicates, orPredicates, name, levelOneParentName, levelTwoParent, queryType);
                        } else {
                            if (queryType == null || !queryType.containsKey(name)) {
                                andPredicates.add(cb.equal(path, value instanceof Date ? (Date) value : value.toString()));
                            } else {
                                switch (QueryType.getItem(queryType.get(name))) {
                                    case EQUALS:
                                        andPredicates.add(cb.equal(path, value instanceof Date ? (Date) value : value.toString()));
                                        break;
                                    case NOT_EQUALS:
                                        andPredicates.add(cb.notEqual(path, value instanceof Date ? (Date) value : value.toString()));
                                        break;
                                    case GREAT:
                                        if (value instanceof Date) {
                                            andPredicates.add(cb.greaterThan(path, (Date) value));
                                        } else if (value instanceof Integer) {
                                            andPredicates.add(cb.greaterThan(path, (Integer) value));
                                        }
                                        break;
                                    case GREAT_OR_EQUALS:
                                        if (value instanceof Date) {
                                            andPredicates.add(cb.greaterThanOrEqualTo(path, (Date) value));
                                        } else if (value instanceof Integer) {
                                            andPredicates.add(cb.greaterThanOrEqualTo(path, (Integer) value));
                                        }
                                        break;
                                    case LESS:
                                        if (value instanceof Date) {
                                            andPredicates.add(cb.lessThan(path, (Date) value));
                                        } else if (value instanceof Integer) {
                                            andPredicates.add(cb.lessThan(path, (Integer) value));
                                        }
                                        break;
                                    case LESS_OR_EQUALS:
                                        if (value instanceof Date) {
                                            andPredicates.add(cb.lessThanOrEqualTo(path, (Date) value));
                                        } else if (value instanceof Integer) {
                                            andPredicates.add(cb.lessThanOrEqualTo(path, (Integer) value));
                                        }
                                        break;
                                    case GT:
                                        if (value instanceof Number) {
                                            andPredicates.add(cb.gt(path, (Number) value));
                                        }
                                        break;
                                    case GE:
                                        if (value instanceof Number) {
                                            andPredicates.add(cb.ge(path, (Number) value));
                                        }
                                        break;
                                    case LT:
                                        if (value instanceof Number) {
                                            andPredicates.add(cb.lt(path, (Number) value));
                                        }
                                        break;
                                    case LE:
                                        if (value instanceof Number) {
                                            andPredicates.add(cb.le(path, (Number) value));
                                        }
                                        break;
                                    case LIKE:
                                        if (value instanceof String) {
                                            andPredicates.add(cb.like(path, Utils.likeQuery(value.toString())));
                                        }
                                        break;
                                    case START_LIKE:
                                        if (value instanceof String) {
                                            andPredicates.add(cb.like(path, Utils.likeStartQuery(value.toString())));
                                        }
                                        break;
                                    case END_LIKE:
                                        if (value instanceof String) {
                                            andPredicates.add(cb.like(path, Utils.likeEndQuery(value.toString())));
                                        }
                                        break;
                                    default:
                                        break;
                                }
                            }
                        }

                    } catch (IllegalAccessException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        };
    }

    private Field getField(Field[] fields, String name) {
        Field rtn = null;
        for (Field field : fields) {
            if (field.getName().equals(name)) {
                rtn = field;
                break;
            }
        }
        return rtn;
    }
}